// Copyright (c) 2022 Presto Labs Pte. Ltd.
// Author: daniel

#include "coin2/exchange/prex/api_order/native_private_client.h"

#include <memory>
#include <string>

#include <fmt/format.h>

#include "coin2/exchange/base/api_util/url_params_builder.h"
#include "coin2/exchange/base/order/order.h"
#include "coin2/exchange/base/order/rate_config.h"
#include "coin2/exchange/prex/symbology/product.h"

constexpr auto EXAMPLE_FUTURES_PREX = R"(
"connection_config": {
  "key_filepath": "/home/ec2-user/config/trade/xunke06/prex/trade_key.json",
  "rate_limit_info": {
    "spot_prex": {
      "req_all": {
        "secs": 300,
        "reqs": 300
      }
    }
  }
})";

auto PrexNativePrivateClient::GetRestOrderParams(
    const OrderSpec& order_spec,
    int64_t proc_order_id,
    int64_t orig_proc_order_id) -> RestParams {
  RestParams params;

  using coin2::exchange::prex::symbology::PrexProduct;
  auto product = PrexProduct::FromStr(order_spec.product, order_spec.order_created_time);
  auto product_holder = GetProductHolder(product, order_spec.order_created_time);
  auto formatter = product_holder->product_info().order_formatter();
  params.set_symbol(order_spec.product);
  if (order_spec.order_type != OrderType::MARKET_ORDER) {
    params.set_price(formatter.FormatPrice(order_spec.price));
  }
  params.set_qty(formatter.FormatQty(order_spec.qty));
  params.set_client_order_id(fmt::format("{}-{}", params.symbol(), proc_order_id));

  if (orig_proc_order_id > 0) {
    params.set_orig_client_order_id(fmt::format("{}-{}", params.symbol(), orig_proc_order_id));
  }

  if (order_spec.order_type == OrderType::MARKET_ORDER) {
    params.set_order_type("ORDER_TYPE_MARKET");
  } else {
    params.set_order_type("ORDER_TYPE_LIMIT");
    // TimeInForce
    //  - TIME_IN_FORCE_INVALID
    //  - TIME_IN_FORCE_GOOD_TILL_CANCEL
    //  - TIME_IN_FORCE_IMMEDIATE_OR_CANCEL
    //  - TIME_IN_FORCE_FILL_OR_KILL
    if (order_spec.duration == coin::proto::OrderDuration::FOK_ORDER) {
      params.set_time_in_force("TIME_IN_FORCE_FILL_OR_KILL");
    } else if (order_spec.duration == coin::proto::OrderDuration::IOC_ORDER) {
      params.set_time_in_force("TIME_IN_FORCE_IMMEDIATE_OR_CANCEL");
    } else {
      params.set_time_in_force("TIME_IN_FORCE_GOOD_TILL_CANCEL");
    }
  }

  // ExecInst:
  //  - EXEC_INST_INVALID
  //  - EXEC_INST_PARTICIPATE_DO_NOT_INITIATE
  //  - EXEC_INST_REDUCE_ONLY
  //  - EXEC_INST_CLOSE
  if (order_spec.post_only) {
    params.set_exec_inst("EXEC_INST_PARTICIPATE_DO_NOT_INITIATE");
  }

  if (order_spec.order_side == OrderSide::BUY_ORDER) {
    params.set_order_side("ORDER_SIDE_BUY");
  } else if (order_spec.order_side == OrderSide::SELL_ORDER) {
    params.set_order_side("ORDER_SIDE_SELL");
  } else {
    NOTREACHED() << "Unknown order side: " << static_cast<int>(order_spec.order_side);
  }

  return params;
}

auto PrexNativePrivateClient::GetRestCancelParams(const IOrderContext& order_context)
    -> RestParams {
  const auto& order_spec = order_context.order_spec();
  auto params = RestParams();
  params.set_symbol(order_spec.product);
  if (order_context.has_exchange_order_id()) {
    params.set_order_id(order_context.exchange_order_id());
  } else {
    params.set_client_order_id(
        fmt::format("{}-{}", params.symbol(), order_context.proc_order_id()));
  }
  return params;
}

auto PrexNativePrivateClient::GetRestAmendParams(
    const OrderSpec& order_spec,
    const std::string& exchange_order_id,
    int64_t proc_order_id,
    int64_t orig_proc_order_id) -> RestParams {
  RestParams params;

  using coin2::exchange::prex::symbology::PrexProduct;
  auto product = PrexProduct::FromStr(order_spec.product, order_spec.order_created_time);
  auto product_holder = GetProductHolder(product, order_spec.order_created_time);
  auto formatter = product_holder->product_info().order_formatter();
  params.set_symbol(order_spec.product);
  if (order_spec.is_price_amended && order_spec.order_type != OrderType::MARKET_ORDER) {
    params.set_price(formatter.FormatPrice(order_spec.price));
  }
  if (order_spec.is_qty_amended) {
    params.set_qty(formatter.FormatQty(order_spec.qty));
  }
  if (!exchange_order_id.empty()) {
    params.set_order_id(exchange_order_id);
  }
  if (proc_order_id > 0) {
    params.set_client_order_id(fmt::format("{}-{}", params.symbol(), proc_order_id));
  }
  if (orig_proc_order_id > 0) {
    params.set_orig_client_order_id(fmt::format("{}-{}", params.symbol(), orig_proc_order_id));
  }

  return params;
}

PrexNativePrivateClient::PrexNativePrivateClient(
    const MarketExchangeApi& mea,
    const NativeClientConfig& config_override) {
  mea_ = mea;
  NativeClientConfig config;
  // config.set_rest_host("api.dev.prex.sh");
  config.set_rest_host("mmapi.aqxapi.com");
  config.set_rest_host_port(443);
  config.MergeFrom(config_override);
  LOG(INFO) << "[PrexNativePrivateClient] config: " << config.DebugString();

  auths_.Construct<BitmexRestAuth, NativeClientConfig>(config);

  // rate limit traker
  using RateConfig = coin2::exchange::base::order::RateConfig;
  coin2::exchange::order::RateLimitInfoSpotPrex rate_config;
  RateConfig::Get()->GetConfig(mea_, config, &rate_config);
  if (!rate_config.has_req_all()) {
    RateConfig::CheckFail(mea_.String(), EXAMPLE_FUTURES_PREX);
  }
  const auto& req_all = rate_config.req_all();
  LOG(INFO) << "\n~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~"
            << "\nOG request rate was set as below:\n"
            << "\nreq_all: [" << req_all.reqs() << " (reqs) / " << req_all.secs() << " (seconds)]"
            << "\n\nOr you can configure customized rate in driver config. For example: "
            << EXAMPLE_FUTURES_PREX
            << "\n~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~";
  rate_tracker_ = std::make_unique<RateTracker>(req_all.secs() * 1'000'000'000LL);
  rate_tracker_->SetRateLimitInfo(req_all.reqs(), req_all.reqs());
  rate_tracker_->SetRateLimitInfoString(&rate_config);
}

std::shared_ptr<AsioHttpContext> PrexNativePrivateClient::QueryWallet() {
  auto ts = GetCurrentTimestamp();
  if (!HasEnoughRateLimit(ts, ActionType::QUERY)) {
    DLOG(ERROR) << "Rate limit! QueryWallet not submit!";
    return {};
  }
  UrlParamsBuilder builder;
  auto* context = GetAuth()->CreateRequestByGetWithSignature("/api/v1/wallet", &builder);
  Push(1, ts, ActionType::QUERY);

  context->query_type = OrderSubTopicId::REST_ACCOUNT_BALANCE;
  return std::shared_ptr<AsioHttpContext>(context);
}

std::shared_ptr<AsioHttpContext> PrexNativePrivateClient::QueryOpenOrders() {
  auto ts = GetCurrentTimestamp();
  if (!HasEnoughRateLimit(ts, ActionType::QUERY)) {
    DLOG(ERROR) << "Rate limit! QueryOpenOrders not submit!";
    return {};
  }
  UrlParamsBuilder builder;
  auto* context = GetAuth()->CreateRequestByGetWithSignature("/api/v1/order", &builder);
  Push(1, ts, ActionType::QUERY);

  context->query_type = OrderSubTopicId::REST_OPEN_ORDERS;
  return std::shared_ptr<AsioHttpContext>(context);
}

std::shared_ptr<AsioHttpContext> PrexNativePrivateClient::QueryOrders(const RestParams& params) {
  auto ts = GetCurrentTimestamp();
  if (!HasEnoughRateLimit(ts, ActionType::QUERY)) {
    DLOG(ERROR) << "Rate limit! QueryOrders not submit!";
    return {};
  }
  UrlParamsBuilder builder;
  builder.PutGet("symbol", params.symbol());
  builder.PutGet("count", params.count());
  auto* context = GetAuth()->CreateRequestByGetWithSignature("/api/v1/order/history", &builder);
  Push(1, ts, ActionType::QUERY);

  context->query_type = OrderSubTopicId::REST_ALL_ORDERS;
  return std::shared_ptr<AsioHttpContext>(context);
}

std::shared_ptr<AsioHttpContext> PrexNativePrivateClient::QueryOrdersByIds(
    const RestParams& params) {
  auto ts = GetCurrentTimestamp();
  if (!HasEnoughRateLimit(ts, ActionType::QUERY)) {
    DLOG(ERROR) << "Rate limit! QueryOrders not submit!";
    return {};
  }
  UrlParamsBuilder builder;
  builder.PutGet("orderIds", params.order_id());
  auto* context =
      GetAuth()->CreateRequestByGetWithSignature("/api/v1/order/historyByIds", &builder);
  Push(1, ts, ActionType::QUERY);

  context->query_type = OrderSubTopicId::REST_ALL_ORDERS;
  return std::shared_ptr<AsioHttpContext>(context);
}

std::shared_ptr<AsioHttpContext> PrexNativePrivateClient::QueryExecution(const RestParams& params) {
  auto ts = GetCurrentTimestamp();
  if (!HasEnoughRateLimit(ts, ActionType::QUERY)) {
    DLOG(ERROR) << "Rate limit! QueryExecution not submit!";
    return {};
  }
  UrlParamsBuilder builder;
  builder.PutGet("symbol", params.symbol());
  builder.PutGet("count", params.count());
  auto* context = GetAuth()->CreateRequestByGetWithSignature("/api/v1/execution/history", &builder);
  Push(1, ts, ActionType::QUERY);

  context->query_type = OrderSubTopicId::REST_FILL_ORDERS;
  return std::shared_ptr<AsioHttpContext>(context);
}

std::shared_ptr<AsioHttpContext> PrexNativePrivateClient::QueryHistConversions(
    const RestParams& params) {
  auto ts = GetCurrentTimestamp();
  if (!HasEnoughRateLimit(ts, ActionType::QUERY)) {
    LOG(ERROR) << "Hit rate limit! QueryHistConversion is not submitted!";
    return {};
  }
  UrlParamsBuilder builder;
  builder.PutGet("count", params.count());
  auto* context =
      GetAuth()->CreateRequestByGetWithSignature("/api/v1/conversion/history", &builder);
  Push(1, ts, ActionType::QUERY);
  context->query_type = OrderSubTopicId::REST_HIST_CONVERSIONS;

  return std::shared_ptr<AsioHttpContext>(context);
}

std::shared_ptr<AsioHttpContext> PrexNativePrivateClient::PlaceOrder(const RestParams& params) {
  auto ts = GetCurrentTimestamp();
  if (!HasEnoughRateLimit(ts, ActionType::PLACE)) {
    DLOG(ERROR) << "Rate limit! PlaceOrder not submit!";
    return {};
  }
  UrlParamsBuilder builder;
  builder.PutPost("symbol", params.symbol());
  builder.PutPost("orderSide", params.order_side());
  builder.PutPost("price", params.price());
  builder.PutPost("qty", params.qty());
  builder.PutPost("orderType", params.order_type());
  builder.PutPost("timeInForce", params.time_in_force());
  builder.PutPost("execInst", params.exec_inst());
  builder.PutPost("clientOrderId", params.client_order_id());
  auto* context = GetAuth()->CreateRequestByPostWithSignature("/api/v1/order/submit", &builder);
  Push(1, ts, ActionType::PLACE);

  context->query_type = OrderSubTopicId::SUBMIT_RESPONSE;
  return std::shared_ptr<AsioHttpContext>(context);
}

std::shared_ptr<AsioHttpContext> PrexNativePrivateClient::AmendOrder(const RestParams& params) {
  auto ts = GetCurrentTimestamp();
  if (!HasEnoughRateLimit(ts, ActionType::AMEND)) {
    LOG(ERROR) << "[AmendOrder] Hit rate limit!";
    return {};
  }

  UrlParamsBuilder builder;
  if (params.has_order_id()) {
    builder.PutPost("orderId", params.order_id());
  } else if (params.has_orig_client_order_id()) {
    builder.PutPost("origClientOrderId", params.orig_client_order_id());
  }
  builder.PutPost("clientOrderId", params.client_order_id());
  builder.PutPost("symbol", params.symbol());
  if (params.has_qty()) {
    builder.PutPost("orderQty", params.qty());
  }
  if (params.has_price()) {
    builder.PutPost("price", params.price());
  }
  auto* context = GetAuth()->CreateRequestByPostWithSignature("/api/v1/order/amend", &builder);
  Push(1, ts, ActionType::AMEND);

  context->query_type = OrderSubTopicId::AMEND_RESPONSE;
  return std::shared_ptr<AsioHttpContext>(context);
}

std::shared_ptr<AsioHttpContext> PrexNativePrivateClient::CancelSingleOrder(
    const RestParams& params) {
  auto ts = GetCurrentTimestamp();
  if (!HasEnoughRateLimit(ts, ActionType::CANCEL)) {
    DLOG(ERROR) << "Rate limit! CancelSingleOrder not submit!";
    return {};
  }
  UrlParamsBuilder builder;
  builder.PutPost("symbol", params.symbol());
  CHECK(params.has_order_id() != params.has_client_order_id())
      << "only one of order_id and client_order_id can be set!" << params.DebugString();

  if (params.has_order_id()) {
    builder.PutPost("orderId", params.order_id());
    auto* context = GetAuth()->CreateRequestByPostWithSignature("/api/v1/order/cancel", &builder);
    Push(1, ts, ActionType::CANCEL);

    context->query_type = OrderSubTopicId::CANCEL_RESPONSE;
    return std::shared_ptr<AsioHttpContext>(context);
  } else {
    builder.PutPost("clientOrderId", params.client_order_id());
    auto* context = GetAuth()->CreateRequestByPostWithSignature("/api/v1/order/cancel", &builder);
    Push(1, ts, ActionType::CANCEL);

    context->query_type = OrderSubTopicId::CANCEL_RESPONSE;
    return std::shared_ptr<AsioHttpContext>(context);
  }
}

std::shared_ptr<AsioHttpContext> PrexNativePrivateClient::CancelOrders(const RestParams& params) {
  auto ts = GetCurrentTimestamp();
  if (!HasEnoughRateLimit(ts, ActionType::CANCEL)) {
    DLOG(ERROR) << "Rate limit! CancelOrders not submit!";
    return {};
  }
  UrlParamsBuilder builder;
  builder.PutPost("symbol", params.symbol());
  auto* context = GetAuth()->CreateRequestByPostWithSignature("/api/v1/order/cancel/all", &builder);
  Push(1, ts, ActionType::CANCEL);

  context->query_type = OrderSubTopicId::MULTI_CANCEL_RESPONSE;
  return std::shared_ptr<AsioHttpContext>(context);
}

std::shared_ptr<AsioHttpContext> PrexNativePrivateClient::PlaceOrderNorm(
    const OrderExecution& order_exe) {
  const auto& order_spec = *order_exe.order_spec;
  const auto proc_order_id = order_exe.proc_order_id;
  auto params = GetRestOrderParams(order_spec, proc_order_id, 0);
  return PlaceOrder(params);
}

std::shared_ptr<AsioHttpContext> PrexNativePrivateClient::CancelOrderNorm(
    const OrderExecution& order_exe) {
  if (order_exe.is_cancel_order_all) {
    if (order_exe.pending_symbol_order_map.size() == 0) {
      return {};
    }
    {
      auto mutable_order_exe = const_cast<OrderExecution*>(&order_exe);
      // this for handleing multiple symbols
      // auto beg = mutable_order_exe->pending_symbol_order_map.begin();
      // auto end = mutable_order_exe->pending_symbol_order_map.end();
      // bool is_full = false;
      // while (beg != end) {
      //   if (!is_full) {
      //     mutable_order_exe->proceed_symbol_order_map.emplace(beg->first, beg->second);
      //     beg = mutable_order_exe->pending_symbol_order_map.erase(beg);
      //     is_full = true;
      //   } else {
      //     ++beg;
      //   }
      // }
      // CHECK(mutable_order_exe->proceed_symbol_order_map.size() == 1)
      //     << "only cancal all order for one symbol per time";
      auto beg = mutable_order_exe->pending_symbol_order_map.begin();
      mutable_order_exe->proceed_symbol_order_map.emplace(beg->first, beg->second);
      mutable_order_exe->pending_symbol_order_map.erase(beg);
    }
    auto symbol_order = order_exe.proceed_symbol_order_map.begin();
    if (symbol_order->second.size() == 0) {
      return {};
    }
    auto params = GetRestCancelParams(*(symbol_order->second.front()));
    return CancelOrders(params);
  } else {
    auto params = GetRestCancelParams(*order_exe.oc);
    return CancelSingleOrder(params);
  }
}

std::shared_ptr<AsioHttpContext> PrexNativePrivateClient::AmendOrderNorm(
    const OrderExecution& order_exe) {
  const auto& order_spec = *order_exe.order_spec;
  const auto proc_order_id = order_exe.proc_order_id;
  const auto orig_proc_order_id = order_spec.orig_proc_order_id;
  const auto& exchange_order_id = order_exe.oc ? order_exe.oc->exchange_order_id() : "";
  auto params =
      GetRestAmendParams(order_spec, exchange_order_id, proc_order_id, orig_proc_order_id);
  return AmendOrder(params);
}

std::shared_ptr<AsioHttpContext> PrexNativePrivateClient::SubmitConversion(
    const ConversionSpec& conv_spec) {
  // params checking
  if (!conv_spec.has_from_currency() || conv_spec.from_currency().empty()) {
    LOG(ERROR) << "[SubmitConversion] mandatory field 'from_currency' is invalid/missing";
    return {};
  }
  if (!conv_spec.has_to_currency() || conv_spec.to_currency().empty()) {
    LOG(ERROR) << "[SubmitConversion] mandatory field 'to_currency' is invalid/missing";
    return {};
  }
  if (!conv_spec.has_from_amount() || conv_spec.from_amount() <= 0) {
    LOG(ERROR) << "[SubmitConversion] mandatory field 'from_amount' is invalid/missing";
    return {};
  }

  auto ts = GetCurrentTimestamp();
  if (!HasEnoughRateLimit(ts, ActionType::PLACE)) {  // TODO(daniel): check action type
    LOG(ERROR) << "Hit rate limit! Conversion is not submitted!";
    return {};
  }
  UrlParamsBuilder builder;
  builder.PutPost("fromCurrency", conv_spec.from_currency());
  builder.PutPost("toCurrency", conv_spec.to_currency());
  builder.PutPost("fromAmount", std::to_string(conv_spec.from_amount()));
  auto* context =
      GetAuth()->CreateRequestByPostWithSignature("/api/v1/conversion/execute", &builder);
  Push(1, ts, ActionType::PLACE);
  context->query_type = OrderSubTopicId::CONVERSION_RESPONSE;

  return std::shared_ptr<AsioHttpContext>(context);
}
